package ru.magicreader.server;

import java.io.BufferedReader;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Date;
import java.util.List;
import java.util.Properties;
import java.util.Map.Entry;

import javax.jdo.PersistenceManager;
import javax.jdo.Transaction;
import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.input.SAXBuilder;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
import org.openid4java.association.AssociationException;
import org.openid4java.consumer.ConsumerException;
import org.openid4java.consumer.ConsumerManager;
import org.openid4java.consumer.VerificationResult;
import org.openid4java.discovery.DiscoveryException;
import org.openid4java.discovery.DiscoveryInformation;
import org.openid4java.discovery.Identifier;
import org.openid4java.message.AuthRequest;
import org.openid4java.message.AuthSuccess;
import org.openid4java.message.MessageException;
import org.openid4java.message.ParameterList;
import org.scribe.builder.ServiceBuilder;
import org.scribe.builder.api.GoogleApi;
import org.scribe.model.OAuthRequest;
import org.scribe.model.Response;
import org.scribe.model.Token;
import org.scribe.model.Verb;
import org.scribe.model.Verifier;
import org.scribe.oauth.OAuthService;

import ru.magicreader.server.model.Feed;
import ru.magicreader.server.model.Settings;
import ru.magicreader.server.model.User;
import ru.magicreader.server.openid.PrepareRequestAttributesFilter;

import com.google.appengine.api.users.UserService;
import com.google.appengine.api.users.UserServiceFactory;
import com.sun.syndication.feed.synd.SyndEntry;
import com.sun.syndication.feed.synd.SyndFeed;
import com.sun.syndication.fetcher.FeedFetcher;
import com.sun.syndication.fetcher.FetcherException;
import com.sun.syndication.fetcher.impl.FeedFetcherCache;
import com.sun.syndication.fetcher.impl.HashMapFeedInfoCache;
import com.sun.syndication.fetcher.impl.HttpURLFeedFetcher;
import com.sun.syndication.io.FeedException;

/**
 * This servlet takes data from FormMain and generates input for tagcloud widget
 */
public class FeedServlet extends HttpServlet {
	private static final String CHECK_AUTH_URI = "check_auth";
	private static final String COLOR_NORMAL = "0x282828";
	private static final String COLOR_IMPORTANT = "0xE44203";
	private static final String COLOR_HIGHLIGHT = COLOR_IMPORTANT;
	private static final String COLOR_READ = "0xCCCCCC";
	
	private static final String INPUT_SESSION_ATTRIBUTE = "input";
	public static final String USER_SESSION_ATTRIBUTE = "user";
	private static final String OAUTH_REQUEST_TOKEN_SESSION_ATTRIBUTE = "request-token";

	private static final long serialVersionUID = 1L;
	private static final String OPENID_DISCOVERY_INFO_SESSION_ATTRIBUTE = "openid-discovery-info";
	private static FeedFetcherCache feedInfoCache = HashMapFeedInfoCache.getInstance();
	private FeedFetcher fetcher = new HttpURLFeedFetcher(feedInfoCache);
	private XMLOutputter xmlOutputter = new XMLOutputter(Format.getPrettyFormat());
	private PersistenceManager persistenceManager;
	private UserService userService;
	private Properties defaultUrlProperties = new Properties();
	private OAuthService oauth;
       
    public FeedServlet() {
        super();
    }

    @Override
	public void init(ServletConfig config) throws ServletException {
    	try {
			persistenceManager = PersistenceFacade.getInstance().getPersistenceManager();
			userService = UserServiceFactory.getUserService();
			defaultUrlProperties.load(getClass().getResourceAsStream("/defaultUrl.properties"));
    	}
    	catch(Exception e) {
    		throw new ServletException(e);
    	}
	}

	@Override
	public void destroy() {
		persistenceManager.close();
	}
	
	private String getBaseAddress(HttpServletRequest request) {
		String requestUrl = request.getRequestURL().toString(); 
		int cutInd = requestUrl.indexOf("/", request.getProtocol().length() + 3);
		String ret = requestUrl.substring(0, cutInd);
		return ret;
	}
	
	private String makeOauthParams(OAuthRequest oauthReq) {
		StringBuilder ret = new StringBuilder();
		for(Entry<String, String> entry : oauthReq.getOauthParameters().entrySet()) {
			ret.append(entry.getKey()).append("=").append(entry.getValue()).append("&");
		}
		
		if(ret.length() > 1) {
			ret.setLength(ret.length() - 1); //cut the last &
		}
		
		return ret.toString();
	}
	
	/**
	 * @see HttpServlet#service(HttpServletRequest request, HttpServletResponse response)
	 */
	protected void service(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
		if(request.getRequestURI().contains("_ah/login_required")) {
			String urlBase = getBaseAddress(request);
			if(oauth == null) {
				oauth = new ServiceBuilder()
					.provider(GoogleApi.class)
					.apiKey("magic-reader.appspot.com")
					.apiSecret("ci8WfFcL0O5BBHT33TNYObuX")
					.callback(urlBase + "/_ah/oauth_callback")
					.scope("https://www.google.com/m8/feeds/")
					.build();
			}
			
			//get the request token
			Token requestToken = oauth.getRequestToken();
			request.getSession().setAttribute(OAUTH_REQUEST_TOKEN_SESSION_ATTRIBUTE, requestToken);
			
			//authorize the request token and get the verifier
			OAuthRequest authorizeReq = new OAuthRequest(Verb.GET, "https://www.google.com/accounts/OAuthAuthorizeToken");
			oauth.signRequest(requestToken, authorizeReq);
			String authorizeReqParams = makeOauthParams(authorizeReq);
			response.sendRedirect(authorizeReq.getUrl() + "?" + authorizeReqParams);
		}
		else if(request.getRequestURI().contains("_ah/oauth_callback")) {
			Token requestToken = (Token)request.getSession().getAttribute(OAUTH_REQUEST_TOKEN_SESSION_ATTRIBUTE);
			String verifierParam = request.getParameter("oauth_verifier");
			if(requestToken == null || verifierParam == null) {
				response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
				return;
			}
			
			//trade the Request Token and Verfier for the Access Token
			Token accessToken = oauth.getAccessToken(requestToken, new Verifier(verifierParam));
			
			//OK, now the user is authenticated. We need to get his email.
			
			//access the user's contacts (protected resource)
			OAuthRequest oauthReq = new OAuthRequest(
					Verb.GET, 
					"https://www.google.com/m8/feeds/contacts/default/full?max-results=0"
			);
			oauth.signRequest(accessToken, oauthReq);
			Response oauthResp = oauthReq.send();
			
			String contactsResponseBody = oauthResp.getBody();
			
			try {
				SAXBuilder xmlParser = new SAXBuilder();
				Document document = xmlParser.build(new ByteArrayInputStream(contactsResponseBody.getBytes()));
				Element idElement = document.getRootElement().getChild("id");
				if(idElement == null) {
					//TODO: log error: cannot discover user's email from OAuth response
					response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
					return;
				}
				
				User user = retrieveUser(idElement.getText()); //get or create user by email
				request.getSession().setAttribute(USER_SESSION_ATTRIBUTE, user);
			}
			catch(JDOMException e) {
				throw new ServletException("Failed to parse user's contacts feed.", e);
			}
		}
		if(request.getRequestURI().contains("_ah/require_auth")) {
			try {
				requestAuthWithOpenId(request, response);
				return;
			}
			catch(Exception e) {
				throw new ServletException(e);
			}
		}
		if(request.getRequestURI().contains(CHECK_AUTH_URI)) {
			try {
				boolean result = checkAuthWithOpenId(request, response);
				PrintWriter out = response.getWriter();
				if(result) {
					out.print("Login successful! " + request.getSession().getAttribute("openid"));
				}
				else {
					out.print("Login failed!");
				}
				out.flush();
				return;
			}
			catch(Exception e) {
				throw new ServletException(e);
			}
		}
		
		if(request.getUserPrincipal() == null) {
			response.setStatus(401); //Unauthorized
			return;
		}
		User user = (User)request.getSession().getAttribute(USER_SESSION_ATTRIBUTE);
		if(user == null) { //not saved in the session yet
			user = retrieveUser(request.getUserPrincipal().getName());
			user.setLogoutUrl(userService.createLogoutURL(userService.createLoginURL("/")));
			request.getSession().setAttribute(USER_SESSION_ATTRIBUTE, user);
		}
		if(request.getSession().getAttribute(INPUT_SESSION_ATTRIBUTE) == null) {
			//generate input
			Document tagcloudInput = generateInput(user, request);
			request.getSession().setAttribute(INPUT_SESSION_ATTRIBUTE, tagcloudInput);
		}
		
		String actionParam = request.getParameter("action");
		try {
			if(actionParam == null) {
				Document input = (Document)request.getSession().getAttribute(INPUT_SESSION_ATTRIBUTE);
				if(input != null) {
					PrintWriter out = response.getWriter();
					xmlOutputter.output(input, out);
					out.flush();
				}
				return;
			}
			else if(actionParam != null && actionParam.equalsIgnoreCase("getUser")) { //getUser request
				Integer numberOfVisits = user.getNumberOfVisits();
				boolean isNewUser = numberOfVisits != null && numberOfVisits.intValue() == 0;
				if(isNewUser && user.getFeeds().size() == 0) {
					String defaultUrl = request.getParameter("defaulturl");
					if(defaultUrl == null)
						defaultUrl = defaultUrlProperties.getProperty(
								request.getLocale().toString(), 
								defaultUrlProperties.getProperty("en")
						);
					
					if(defaultUrl != null) {
						user.getFeeds().add(new Feed(defaultUrl, false));
						persistenceManager.makePersistent(user); //to generate feed id
						
						//generate input
						Document tagcloudInput = generateInput(user, request);
						request.getSession().setAttribute(INPUT_SESSION_ATTRIBUTE, tagcloudInput);
					}
				}
				user.incrementNumberOfVisits();
				persistenceManager.makePersistent(user);
				
				response.setCharacterEncoding("UTF-8");
				PrintWriter out = response.getWriter();
				out.print(user.toJson());
				out.flush();
			}
			else if(actionParam != null && actionParam.equalsIgnoreCase("newFeed")) { //newFeed request
				//add new feed to user feeds
				String newFeedUrl = request.getReader().readLine();
				if(newFeedUrl == null) throw new ServletException("No feed URL provided.");
				String newFeedImportant = request.getReader().readLine();
				List<Feed> feeds = user.getFeeds();
				Feed newFeed = new Feed(newFeedUrl, "important=true".equals(newFeedImportant));
				feeds.add(newFeed);
				persistenceManager.makePersistent(user);
				
				//generate input
				Document tagcloudInput = generateInput(user, request);
				request.getSession().setAttribute(INPUT_SESSION_ATTRIBUTE, tagcloudInput);
				
				//reply with JSON
				response.setCharacterEncoding("UTF-8");
				PrintWriter out = response.getWriter();
				out.print(newFeed.toJson());
				out.flush();
			}
			else if(actionParam != null && actionParam.equalsIgnoreCase("modifyFeed")) {
				//modify feed properties
				String modifyFeedUrl = request.getReader().readLine();
				if(modifyFeedUrl == null) throw new ServletException("No feed URL provided.");
				String important = request.getReader().readLine();
				for(Feed feed : user.getFeeds()) {
					if(modifyFeedUrl.equals(feed.getUrl())) {
						feed.setImportant("important=true".equals(important));
						break;
					}
				}
				persistenceManager.makePersistent(user);
				
				//generate input
				Document tagcloudInput = generateInput(user, request);
				request.getSession().setAttribute(INPUT_SESSION_ATTRIBUTE, tagcloudInput);
				response.setStatus(200); //OK
			}
			else if(actionParam != null && actionParam.equalsIgnoreCase("deleteFeed")) {
				//remove feed from user feeds
				String deleteFeedUrl = request.getReader().readLine();
				Feed toRemove = null;
				List<Feed> feeds = user.getFeeds();
				for(Feed feed : feeds) {
					if(feed.getUrl().equals(deleteFeedUrl)) {
						toRemove = feed; break;
					}
				}
				if(toRemove == null) {
					response.setStatus(500); //Internal Server Error
					return;
				}
				feeds.remove(toRemove);
				
				//remove the object and save the parent state
				Transaction tx = persistenceManager.currentTransaction();
				try {
					tx.begin();
					persistenceManager.deletePersistent(
							persistenceManager.getObjectById(toRemove.getClass(), toRemove.getId()) 
					); 
					persistenceManager.makePersistent(user);
					tx.commit();
				} catch (Exception e) {
					if (tx.isActive()) {
						tx.rollback();
					}
					throw new ServletException(e);
				}
				
				//generate input
				Document tagcloudInput = generateInput(user, request);
				request.getSession().setAttribute(INPUT_SESSION_ATTRIBUTE, tagcloudInput);
				response.setStatus(200); //OK
			}
			else if(actionParam != null && actionParam.equalsIgnoreCase("read")) {
				try {
					String sid = request.getParameter("id");
					String sindex = request.getParameter("index");
					if(sid == null || sindex == null) {
						response.setStatus(400); //Bad request
						return;
					}
					
					long id = Long.parseLong(sid);
					int index = Integer.parseInt(sindex);
					
					//find the feed
					for(Feed feed : user.getFeeds()) {
						if(feed.getId().getId() == id) {
							//found the one, read it
							SyndEntry entry = (SyndEntry)feed.getInternal().getEntries().get(index); //let go the exception if index is wrong
							Date publishedDate = entry.getPublishedDate();
							if(publishedDate != null)
								feed.addReadEntryDate(publishedDate);
							persistenceManager.makePersistent(user);
							Document tagcloudInput = generateInput(user, request);
							request.getSession().setAttribute(INPUT_SESSION_ATTRIBUTE, tagcloudInput);
							response.sendRedirect(entry.getLink());
							return;
						}
					}
					
					//feed not found
					response.setStatus(404); //Not found
				}
				catch(NumberFormatException e) {
					response.setStatus(400); //Bad request
				}
			}
			else if(actionParam != null && actionParam.equalsIgnoreCase("updateFeeds")) {
				//generate input
				Document tagcloudInput = generateInput(user, request);
				request.getSession().setAttribute(INPUT_SESSION_ATTRIBUTE, tagcloudInput);
				response.setStatus(200); //OK
			}
			else if(actionParam != null && actionParam.equalsIgnoreCase("modifySettings")) {
				try {
					BufferedReader reader = request.getReader();
					String line = null;
					while((line = reader.readLine()) != null) {
						if(line != null && line.startsWith("nEntriesToShow=")) {
							String sn = line.substring("nEntriesToShow=".length());
							user.getSettings().setNEntriesToShow(Integer.parseInt(sn));
						}
					}
					persistenceManager.makePersistent(user);
					
					//generate input
					Document tagcloudInput = generateInput(user, request);
					request.getSession().setAttribute(INPUT_SESSION_ATTRIBUTE, tagcloudInput);
					response.setStatus(200); //OK
				}
				catch(NumberFormatException e) {
					response.setStatus(400); //Bad request
				}
			}
			else if(actionParam != null && actionParam.equalsIgnoreCase("logout")) {
				request.getSession().invalidate();
				response.sendRedirect(user.getLogoutUrl());
			}
			else { //request for input
				response.setStatus(400); //Bad request
			}
		}
		catch(MalformedURLException e) {
			throw new ServletException(e);
		}
	}
	
	private void requestAuthWithOpenId(HttpServletRequest request, HttpServletResponse response) throws DiscoveryException, MessageException, ConsumerException, IOException {
		ConsumerManager consumerManager = 
			(ConsumerManager)request.getAttribute(PrepareRequestAttributesFilter.CONSUMERMANAGER_REQUEST_ATTRIBUTE);
		List<?> discoverList = consumerManager.discover("https://www.google.com/accounts/o8/id");
		DiscoveryInformation discoveryInformation = consumerManager.associate(discoverList);
		request.getSession().setAttribute(OPENID_DISCOVERY_INFO_SESSION_ATTRIBUTE, discoveryInformation);
		
//		String continueUrl = request.getParameter("continue");
		
		String checkAuthUrl = 
			request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort()
			+ "/" + CHECK_AUTH_URI;
		AuthRequest authRequest = consumerManager.authenticate(discoveryInformation, checkAuthUrl);
		response.sendRedirect(authRequest.getDestinationUrl(true));
	}
	
	private boolean checkAuthWithOpenId(HttpServletRequest request, HttpServletResponse response) throws MessageException, DiscoveryException, AssociationException, IOException {
		ConsumerManager consumerManager = 
			(ConsumerManager)request.getAttribute(PrepareRequestAttributesFilter.CONSUMERMANAGER_REQUEST_ATTRIBUTE);
		ParameterList responselist = new ParameterList(request.getParameterMap());
		DiscoveryInformation discoveryInformation = 
			(DiscoveryInformation)request.getSession().getAttribute(OPENID_DISCOVERY_INFO_SESSION_ATTRIBUTE);
		
        StringBuffer receivingURL = request.getRequestURL();
        String queryString = request.getQueryString();
        if (queryString != null && queryString.length() > 0)
            receivingURL.append("?").append(request.getQueryString());

        VerificationResult verification = consumerManager.verify(
                receivingURL.toString(),
                responselist, 
                discoveryInformation
        );

        // examine the verification result and extract the verified identifier
        Identifier verified = verification.getVerifiedId();
        if (verified != null) {
            AuthSuccess authSuccess = (AuthSuccess) verification.getAuthResponse();

            request.getSession().setAttribute("openid", authSuccess.getIdentity());
            request.getSession().setAttribute("openid-claimed", authSuccess.getClaimed());
//            response.sendRedirect(".");  // success
            return true;
        }
        else return false;
	}

	private User retrieveUser(String login) {
		User ret = null;
		try {
			ret = persistenceManager.getObjectById(User.class, login);
		}
		catch(Throwable e) {}
		if(ret != null) {
			if(ret.getSettings() == null) ret.setSettings(new Settings()); //migrate to new data schema
			return ret;
		}
		
		return persistenceManager.makePersistent(new User(login));
	}

	@SuppressWarnings("unchecked")
	private Document generateInput(User user, HttpServletRequest request) 
	throws IllegalArgumentException, IOException {
		Document ret = new Document();
		Element root = new Element("tags");
		ret.setRootElement(root);
		
		for(Feed feed : user.getFeeds()) {
			try {
				SyndFeed syndFeed = fetcher.retrieveFeed(new URL(feed.getUrl()));
				
				feed.setTitle(syndFeed.getTitle());
				feed.setValid(true);
				feed.setInternal(syndFeed);
				
				List<SyndEntry> feedEntries = syndFeed.getEntries();
				int count = 0;
				for(SyndEntry entry : feedEntries) {
					Element a = new Element("a");
					//a.setAttribute("href", entry.getLink());
					a.setAttribute(
							"href", 
							getBaseUrl(request) + 
							"?action=read&id=" + feed.getId().getId() + "&index=" + count
					);
					
					int fontSize = 5;
					if(count == 0) { //first element
						fontSize = 12;
					}
					else if(count > 0 && count < 4) { //next 3 elements
						fontSize = 9;
					}
					a.setAttribute("style", "font-size: " + fontSize + "pt;");
					
					String color = null;
					boolean entryIsRead = feed.getReadEntryDates().contains(entry.getPublishedDate());
					if(entryIsRead) {
						color = COLOR_READ;
					}
					else if(feed.isImportant() && count == 0) {
						color = COLOR_IMPORTANT;
					}
					else {
						color = COLOR_NORMAL;
					}
					
					a.setAttribute("color",  color);
					a.setAttribute("hicolor", entryIsRead ? COLOR_READ : COLOR_HIGHLIGHT);
					a.setAttribute("target", "_blank");
					
					a.setText(entry.getTitle());
					
					root.addContent(a);
					
					if(++count == user.getSettings().getNEntriesToShow()) break;
				}
			}
			catch(MalformedURLException e) {
				feed.setValid(false);				
			}
			catch(FetcherException e) {
				feed.setValid(false);
			}
			catch(FeedException e) {
				feed.setValid(false);
			}
			catch(IOException e) {
				feed.setValid(false);
			}
		}
		
		return ret;
	}
	
	private String getBaseUrl(HttpServletRequest request) {
		if(request.getServerPort() == 80 || request.getServerPort() == 443) {
			return request.getScheme() + "://" +
			request.getServerName() +
			request.getRequestURI();
		}

		return request.getScheme() + "://" +
			request.getServerName() + ":" + request.getServerPort() +
			request.getRequestURI();
	}

}
